const gettype = Object.prototype.toString;
const TYPE_NIL = 0
const TYPE_BOOLEAN = 1

const TYPE_NUMBER = 2
const TYPE_NUMBER_ZERO = 0
const TYPE_NUMBER_BYTE = 1
const TYPE_NUMBER_WORD = 2
const TYPE_NUMBER_DWORD = 4
const TYPE_NUMBER_QWORD = 6
const TYPE_NUMBER_REAL = 8

const TYPE_USERDATA = 3

const TYPE_SHORT_STRING = 4
const TYPE_LONG_STRING = 5

const TYPE_TABLE = 6

const BLOCK_SIZE = 1024
const MAX_COOKIE = 32

function COMBINE_TYPE(t, v) {
	return t | (v << 3)
}

/*
 * gettype.call('aaaa')输出      [object String]
 * gettype.call(2222) 输出      [object Number]
 * gettype.call(true)  输出      [object Boolean]
 * gettype.call(undefined)  输出      [object Undefined]
 * gettype.call(null)  输出   [object Null]
 * gettype.call({})   输出   [object Object]
 * gettype.call([])    输出   [object Array]
 * gettype.call(function(){})     输出   [object Function]
*/
function checktype(value, type) {
    var typestr = gettype.call(value)
    var cmp = typestr.substring(8, typestr.length - 1)
    cmp = cmp.toLowerCase()
    if (cmp == type) {
        return true
    } else {
        return cmp
    }
}

function isinteger(obj) {
	return typeof obj === 'number' && obj%1 === 0
}

function uint64_rshift(num, offset) {
    return Math.floor(num / Math.pow(2, offset))
}

// 检测整数需要编码的字节数
function integer_byte(num, offset) {
    if (offset === void 0) { 
    	offset = 31 
    }

    var numh = uint64_rshift(num, offset);
    if (numh === 0 || numh === -1) {
        return 4
    } else {
        return 8
    }
}

function wb_check(size, wb) {
	var len = wb.buffer.length - wb.offset
	while (len < size) {
		var newsize = wb.buffer.length << 1
		newbuf = Buffer.allocUnsafe(newsize)
		newbuf.fill(wb.buffer, 0, wb.buffer.length)
		wb.buffer = newbuf
	}
}

function wb_byte(v, wb) {
	wb_check(1, wb)
	wb.buffer[wb.offset++] = v
}

function wb_nil(wb) {
	wb_byte(TYPE_NIL, wb)
}

function wb_integer(number, wb) {
	var type = TYPE_NUMBER
	var n
	if (number == 0) {
		// 0
		n = COMBINE_TYPE(type, TYPE_NUMBER_ZERO)
		wb_byte(n, wb)
	} else if (integer_byte(number) == 8) {
		// int64
		// 8 个字节数字会有问题，建议转成字符串来传输
		n = COMBINE_TYPE(type, TYPE_NUMBER_QWORD)
		wb_byte(n, wb)
		wb.offset = wb.buffer.writeDoubleLE(number, wb.offset)
	} else if (number < 0) {
		// int32
		n = COMBINE_TYPE(type, TYPE_NUMBER_DWORD)
		wb_byte(n, wb)
		wb.offset = wb.buffer.writeInt32LE(number, wb.offset)
	} else if (number < 0x100) {
		// uint8
		n = COMBINE_TYPE(type, TYPE_NUMBER_BYTE)
		wb_byte(n, wb)
		wb.offset = wb.buffer.writeUInt8(number, wb.offset)
	} else if (number < 0x10000) {
		// uint16
		n = COMBINE_TYPE(type, TYPE_NUMBER_WORD)
		wb_byte(n, wb)
		wb.offset = wb.buffer.writeUInt16LE(number, wb.offset)
	} else {
		// uint32
		n = COMBINE_TYPE(type, TYPE_NUMBER_DWORD)
		wb_byte(n, wb)
		wb.offset = wb.buffer.writeUInt32LE(number, wb.offset)
	}
}

function wb_real(number, wb) {
	var n = COMBINE_TYPE(TYPE_NUMBER, TYPE_NUMBER_REAL)
	wb_byte(n, wb)
	wb.offset = wb.buffer.writeDoubleLE(number, wb.offset)
}

function wb_boolean(bool, wb) {
	var n = COMBINE_TYPE(TYPE_BOOLEAN, bool ? 1 : 0)
	wb_byte(n, wb)
}

function wb_string(value, wb) {
	value = Buffer.from(value)
	var len = value.length
	if (len < MAX_COOKIE) {
		var n = COMBINE_TYPE(TYPE_SHORT_STRING, len)
		wb_byte(n, wb)
		if (len > 0) {
			wb.buffer.fill(value, wb.offset, wb.offset+len)
			wb.offset += len
		}
	} else {
		if (len < 0x10000) {
			var n = COMBINE_TYPE(TYPE_LONG_STRING, 2)
			wb_byte(n, wb)
			wb.offset = wb.buffer.writeUInt16LE(len, wb.offset)
		} else {
			var n = COMBINE_TYPE(TYPE_LONG_STRING, 4)
			wb_byte(n, wb)
			wb.offset = wb.buffer.writeUInt32LE(len, wb.offset)
		}
		wb.buffer.fill(value, wb.offset, wb.offset+len)
		wb.offset += len
	}
}

function wb_array(array, wb) {
	var length = array.length
	if (length >= MAX_COOKIE-1) {
		var n = COMBINE_TYPE(TYPE_TABLE, MAX_COOKIE-1)
		wb_byte(n, wb)
		wb_integer(length, wb)
	} else {
		var n = COMBINE_TYPE(TYPE_TABLE, length)
		wb_byte(n, wb)
	}

	for (var i = 0; i < length; i++) {
		pack_one(array[i], wb)
	}

	wb_nil(wb)  // 做为 [] 与其他变量的分界符
}

function wb_object(obj, wb) {
	var n = COMBINE_TYPE(TYPE_TABLE, 0)
	wb_byte(n, wb)

	for (var i in obj) {
		pack_one(i, wb)
		pack_one(obj[i], wb)
	}

	wb_nil(wb)  // 做为 {} 与其他变量的分界符
}

function pack_one(value, wb) {
	var type = checktype(value)
	switch(type) {
	case "null":
		wb_nil(wb)
		break
	case "number":
		if (isinteger(value)) {
			wb_integer(value, wb)
		} else {
			// double number
			wb_real(value, wb)
		}
		break
	case "boolean":
		wb_boolean(value, wb)
		break
	case "string":
		wb_string(value, wb)
		break
	case "object":
		wb_object(value, wb)
		break
	case "array":
		wb_array(value, wb)
		break
	default:
		console.log("Unsupport type: " + type + " to serialize")
		break
	}
}

function skynet_pack(data) {
	if (!checktype(data, "object") || !checktype(data, "array")) {
		console.warn("pack data type must be object")
		return
	}

	var wb = {}
	wb.buffer = Buffer.allocUnsafe(BLOCK_SIZE)
	wb.offset = 0 //初始化 偏移量

	wb_object(data, wb)

	var buff = Buffer.allocUnsafe(wb.offset)
	wb.buffer.copy(buff, 0, 0, wb.offset)
	return buff
}

function get_integer(buffer, offset, cookie) {
	switch(cookie) {
	case TYPE_NUMBER_REAL:
		var value = buffer.readDoubleLE(offset)
		return {size: 8, value}
	case TYPE_NUMBER_BYTE:
		var value = buffer.readUInt8(offset)
		return {size: 1, value}
	case TYPE_NUMBER_WORD:
		var value = buffer.readUInt16LE(offset)
		return {size: 2, value}
	case TYPE_NUMBER_DWORD:
		var value = buffer.readInt32LE(offset)		
		return {size: 4, value}
	case TYPE_NUMBER_QWORD:
		var value = buffer.readDoubleLE(offset)
		return {size: 8, value}
	default:
		console.log("Invalid serialize stream cookie: " + cookie)
		break
	}
}

function unpack_array(buffer, offset, cookie) {
	var length = 0
	var oldoffset = offset
	var value = []
	var type

	if (cookie == MAX_COOKIE-1) {
		type = buffer[offset++]
		var numtb = unpack_one(buffer, offset, type & 0x07, type >> 3)
		if (numtb) {
			offset += numtb.size
			length = numtb.value
		} else {
			console.log("read array size failed")
		}
	} else {
		length = cookie
	}

	for (var i = 0; i < length; i++) {
		type = buffer[offset++]
		var v = unpack_one(buffer, offset, type & 0x07, type >> 3)
		offset += v.size
		value[i] = v.value
	}
	return {size: offset - oldoffset + 1, value}
}

function unpack_object(buffer, offset) {
	var idx = offset
	var type
	var obj = {}

	while (idx < buffer.length) {
		type = buffer[idx++]
		var one = unpack_one(buffer, idx, type & 0x07, type>>3)
		if (!one || one.nil) {
			break
		}
		idx += one.size

		type = buffer[idx++]
		var two = unpack_one(buffer, idx, type & 0x07, type>>3)
		if (!two) {
			break
		}
		idx += two.size

		obj[one.value] = two.value

		type = buffer[idx+1]
		if ((type & 0x07) == TYPE_NIL) {
			++idx
			break
		}
	}

	return {size: idx - offset, value: obj}
}

function unpack_one(buffer, offset, type, cookie) {
	switch(type) {
	case TYPE_SHORT_STRING:
		var value = buffer.toString("utf8", offset, offset + cookie)
		return {size: cookie, value}
	case TYPE_NUMBER:
		var tmp = get_integer(buffer, offset, cookie)
		return tmp
	case TYPE_LONG_STRING:
		if (cookie == 2) {
			var len = buffer.readUInt16LE(offset)
			offset += 2
			var value = buffer.toString("utf8", offset, offset + len)
			return {size: len, value}
		} else if (cookie == 4) {
			var len = buffer.readUInt32LE(offset)
			offset += 4
			var value = buffer.toString("utf8", offset, offset + len)
			return {size: len, value}
		}
	case TYPE_NIL:
		return {size: 0, nil: true}
	case TYPE_BOOLEAN:
		var value = cookie == 1 ? true : false
		return {size: 0, value}
	case TYPE_TABLE:
		if (cookie > 0) {
			return unpack_array(buffer, offset, cookie)
		} else {
			return unpack_object(buffer, offset)
		}
	}
}

function skynet_unpack(buffer) {
	var type = buffer[0]
	if ((type & 0x07) !== TYPE_TABLE) {
		console.log("unpack data must be a object, type: " + type)
		return
	}

	var result = unpack_object(buffer, 1)
	return result.value
}

module.exports = {
	pack: skynet_pack,
	unpack: skynet_unpack,
	checktype: checktype,
}
